﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Input;

using Lidgren.Network;

namespace TradeSea
{

    public static class ClientBoats
    {
        static Dictionary<int, Boat> Boats = new Dictionary<int, Boat>();

        public static MainGame owner;
        public static Model boatModel; 

        public static void UpdateBoat(float rudder, float yard, int uid)
        {
            if (owner == null)
            {
                return;
            }
            if (!Boats.ContainsKey(uid))
            {
                Boats.Add(uid, new Boat(owner, boatModel));
            }
            Boats[uid].NetUpdate(rudder, yard);
        }

        public static void Update(World gameWorld, float deltaTime)
        {
            foreach (Boat item in Boats.Values)
            {
                item.Update(new KeyboardState(), gameWorld, deltaTime);
            }
        }

        public static void Draw(GraphicsDevice device, Camera cam)
        {
            foreach (KeyValuePair<int, Boat> item in Boats)
            {
                if (TradeSeaNetClient.Singilton.Client.Port == item.Key)
                {
                    continue;
                }
                item.Value.Draw(device, cam.getProjection(), cam.getView());
            }
        }
    }

    public class Boat
    {
        Model shipModel;
        List<ModelMesh> staticParts = new List<ModelMesh>(); //parts that don't move go here
        List<ModelMesh> yardarms = new List<ModelMesh>(); //arms on masts that allowed ships to rotate their sails
        List<ModelMesh> rudders = new List<ModelMesh>(); //
        List<ModelMesh> wheel = new List<ModelMesh>(); //the steering wheel, could be a list but whatever
        Vector3 location = new Vector3(0, 0, 0); //information like this will get moved to a physics/parent class?
        float heading = MathHelper.PiOver2; //heading in radians

        MainGame owner;

        float mass = 50000; //weight in kg
        private float speed = 0; //speed in m/s
        public float rotated = 0;
        float sailArea = 392; //approx sail area in square metres
        public float yardAngle = 0; //current angle of yardarms
        public float rudderAngle = 0f; //current angle of rudder

        float rudderTurnRate = MathHelper.PiOver4; //rates at which parts can turn, and limits, are defined here
        float yardTurnRate = MathHelper.PiOver4;
        float yardLimit = MathHelper.ToRadians(37);
        float rudderLimit = MathHelper.ToRadians(70);

        float heelAngle = 0; //Angle the ship is leaning to the side/not worked into physics but looks ok
        float heelVel = 0; //Speed at which the ship is currently rotating

        bool wheelHeld = true;

        Vector3 FPCameraPos = new Vector3(0);
        public Boat(MainGame game, Model m)
        {
            this.owner = game;
            shipModel = m;
            preProcessModel();
        }

        public float Speed
        {
            get
            {
                return speed;
            }
        }

        public Vector3 direction //direction the boat is facing
        {
            get
            {
                return Vector3.Transform(Vector3.Forward, Matrix.CreateRotationY(heading));
            }
            set
            {
                direction = value;
            }
        }
        public Vector3 Velocity
        {
            get { return speed * direction; }
        }
        public Vector3 yardDirection //direction yard/sails are facing
        {
            get
            {
                return Vector3.Transform(Vector3.Forward, Matrix.CreateRotationY(heading + yardAngle) * Matrix.CreateRotationZ(heelAngle));
            }
        }

        public void adjustYardAngle(bool clockwise, float deltaTime)
        {
            if (clockwise)
                yardAngle += yardTurnRate * deltaTime;
            else yardAngle -= yardTurnRate * deltaTime;
            if (yardAngle > yardLimit)
                yardAngle = yardLimit;
            else if (yardAngle < -yardLimit)
                yardAngle = -yardLimit;
        } //lets yard and rudder be adjusted at a certain turn rate
        public void adjustRudderAngle(bool clockwise, float deltaTime)
        {
            if (clockwise)
                rudderAngle += rudderTurnRate * deltaTime;
            else rudderAngle -= rudderTurnRate * deltaTime;
            if (rudderAngle > rudderLimit)
                rudderAngle = rudderLimit;
            else if (rudderAngle < -rudderLimit)
                rudderAngle = -rudderLimit;
        }
        public void adjustRudderAngle(float angle)
        {
            rudderAngle += angle;
            if (rudderAngle > rudderLimit)
                rudderAngle = rudderLimit;
            else if (rudderAngle < -rudderLimit)
                rudderAngle = -rudderLimit;
        }
        public Vector3 Location
        {
            get
            {
                return location;
            }
            set
            {
                location = value;
            }
        }
        public Vector4 FPCameraLocAngle //returns location and angle of fp camera
        {
            get
            {
                return new Vector4(location + Vector3.Transform(FPCameraPos, Matrix.CreateRotationY(heading)), heading);
            }
        }

        public float getCompassHeading()
        {
            float compassHeading = -heading;
            compassHeading = compassHeading % MathHelper.TwoPi;
            if (compassHeading < 0)
                compassHeading += MathHelper.TwoPi;
            return compassHeading;
        }

        public Boat(Model m, Vector3 location, Vector3 dir)
        {
            this.location = location;
            this.direction = dir;
            shipModel = m;
            preProcessModel();
        }

        void preProcessModel()
        {
            try
            {
                foreach (ModelMesh mesh in shipModel.Meshes) //Go through each mesh and assign it to proper group, so they can have different transforms applied
                {
                    string[] input = mesh.Name.Split('_');
                    switch (input[0])
                    {
                        case "yard":
                            yardarms.Add(mesh);
                            break;
                        case "rudder":
                            rudders.Add(mesh);
                            break;
                        case "wheel":
                            wheel.Add(mesh);
                            break;
                        case "camera":
                            if (input[1] == "fp")
                                FPCameraPos = Vector3.Transform(mesh.ParentBone.Transform.Translation, mesh.ParentBone.Parent.Transform); //this step rotates bone by parent bone so its in the xna xyz system, before storing
                            break;
                        default:
                            staticParts.Add(mesh);
                            break;
                    }
                }
            }
            catch
            {
                throw new Exception("Model Names Invalid");
            }
        }

        public void NetUpdate(float yard, float rudder)
        {
            this.yardAngle = yard;
            this.rudderAngle = rudder;
        }

        public void Update(KeyboardState kstate, World world, float deltaTime)
        {
            //input
            wheelHeld = true;
            if (kstate.IsKeyDown(Keys.A))//rudder controls, separate from AD at the moment because they're convenient for testing.
                this.adjustRudderAngle(true, deltaTime);
            if (kstate.IsKeyDown(Keys.D))
                this.adjustRudderAngle(false, deltaTime);
            if (kstate.IsKeyDown(Keys.Q)) //yard controls
                this.adjustYardAngle(true, deltaTime);
            if (kstate.IsKeyDown(Keys.W))
                this.adjustYardAngle(false, deltaTime);
            if (kstate.IsKeyDown(Keys.S)) //temporary
                wheelHeld = false;
            //if (kstate.IsKeyDown(Keys.B)) //temporary
            //    this.speed = 100;
            //if (kstate.IsKeyDown(Keys.E)) //temporary
            //    this.speed = 400;
            // Joshua.S - There cool and all but they corrupt the network state

            doPhysics(world, deltaTime);
            
        }

        void doPhysics(World world, float deltaTime)
        {
            if (!wheelHeld && Math.Abs(rudderAngle) > 0.001f)
            {
                adjustRudderAngle((rudderAngle < 0), deltaTime);
                if (rudderAngle < 0.005f && rudderAngle > -0.005f)
                    rudderAngle = 0;
            }
            
            Vector3 forceOnSails = getForceOnSail(world);
            if (forceOnSails.LengthSquared() != 0)
            {
                float boatAccel = Vector3.Dot(direction, Vector3.Normalize(forceOnSails)) * forceOnSails.Length() / mass * deltaTime; //work out acceleration from sails

                speed += boatAccel; //apply component of sail acceleration in line with ship to speed (its assumed all sideways force is offset by keel etc)

                //not actual torque idk what to do for inertia but works ok
                float sideForce = Vector3.Dot(Vector3.Transform(Vector3.Left, Matrix.CreateRotationY(heading)), forceOnSails);

                heelVel += sideForce / mass * deltaTime;
            }
 //turns the ship kindof working
            this.heading += speed * rudderAngle / Math.Max(30, Math.Min(speed * speed, 180)) * deltaTime;
            /*if(this.heading >= 2*(float)(Math.PI))
            {
                this.heading -= 2*(float)(Math.PI);
            }
            else if(this.heading < 0)
            {
                this.heading += 2*(float)(Math.PI);
            }*/
            this.heading = heading % MathHelper.TwoPi;
            if (this.heading < 0)
                heading += MathHelper.TwoPi;

            this.speed -= rudderAngle * rudderAngle * speed * Math.Abs(speed) * 500 /mass * deltaTime;
            
            heelVel += -rudderAngle * speed / 60f * deltaTime;
            heelVel -= heelAngle / MathHelper.PiOver4 * Math.Abs(heelAngle / MathHelper.PiOver4 * 4) * 1.3f * deltaTime;//counterheelforce
            heelVel -= 0.35f * heelVel * deltaTime; //angular velocity decay
            heelAngle += heelVel * deltaTime; //apply heel


            float dragMulti = 35;
            if (speed < 0)
                dragMulti *= 3;
            speed -= speed * Math.Abs(speed) * dragMulti / mass * deltaTime; //water drag on ship with arbitrary values
            
            location += direction * speed * deltaTime;//applies speed to the thingy
        }

        public Vector3 getForceOnSail(World world) //gets the force on the sail in the direction of the sail and stuff
        {
            Vector3 wind = world.GetWind(this.location); //get wind world co-ords
            wind -= Velocity; //get relative wind speed
            if (wind.LengthSquared() == 0)
                return new Vector3(0);

            float windYardDot = Vector3.Dot(Vector3.Normalize(wind), Vector3.Normalize(yardDirection)); //How much is the wind aligned with the sail
            Vector3 ForceOnSail = new Vector3(0);

            //LIFT APPROX //unfinished/wrong
            /*
            float cl = MathHelper.TwoPi * (heading + yardAngle) * 0.4f; //thin line airfoil approximation with a made up constant aerodynamic coefficient
            Vector3 liftDirection = new Vector3(0);
            liftDirection = Vector3.Transform(Vector3.Forward, Matrix.CreateRotationY(heading + yardAngle));
            float lift = 0.5f * airDensity * wind.LengthSquared() * sailArea * cl * 0.6f * Vector3.Dot(liftDirection, yardDirection); //lift formula //needs stall angle built in bleh*/
            //Round 2 - shitty approx
            float windLiftForce = 0;
            if (Math.Abs(windYardDot) > 0.001 && Math.Abs(windYardDot) < 0.38)
            {
                float strengthTerm = 1 - Math.Abs((windYardDot - 0.34f) * (1f/0.38f)); //    graph -> /\
                windLiftForce = sailArea * strengthTerm * wind.LengthSquared() * world.GetAirDensity(this.location) * 0.8f;
                if (windYardDot < 0)
                    windLiftForce *= 0.1f;
                ForceOnSail += yardDirection * windLiftForce; //apply force from pull on sail due to lift like on a plane wing
            }


            //Drag approx

            float windDragForce = windYardDot * sailArea * wind.LengthSquared() * /*world.GetAirDensity(this.location) */ 0.95f;
            //approximated by 2d area of sail facing wind * wind speed squared * air density * times arbitrary energy efficiency coefficent
            windDragForce = windDragForce * Math.Min(1, 9 * windYardDot * windYardDot); //ad hoc account for sail flapping uselessly at close to 90 degree angles to the wind
            if (windDragForce < 0)
                windDragForce *= 0.6f;

            ForceOnSail += windDragForce * yardDirection; //Apply force from wind "pushing" the boat
            //Console.WriteLine(windLiftForce.ToString() + "   " + windDragForce.ToString());
            //Console.Write(windDragForce + " " + speed + '\r');
            return ForceOnSail;
        }

        public void Draw(GraphicsDevice device, Matrix projection, Matrix view) //draw everything
        {
            device.DepthStencilState = DepthStencilState.Default;
            Matrix[] transforms = new Matrix[shipModel.Bones.Count];
            shipModel.CopyAbsoluteBoneTransformsTo(transforms);

            SamplerState s = new SamplerState();
            s.Filter = TextureFilter.Anisotropic;
            device.SamplerStates[0] = s;

            Matrix ShipTransform = Matrix.CreateRotationZ(heelAngle) * Matrix.CreateRotationY(heading) * Matrix.CreateTranslation(location) ;

            foreach (ModelMesh mesh in staticParts)
            {
                foreach (BasicEffect effect in mesh.Effects)
                {
                    effect.EnableDefaultLighting();
                    effect.View = view;
                    effect.Projection = projection;
                    Matrix temp = transforms[mesh.ParentBone.Index];
                    effect.World = temp * ShipTransform;
                    
                }
                mesh.Draw();
            }
            foreach (ModelMesh mesh in yardarms)
            {
                foreach (BasicEffect effect in mesh.Effects)
                {
                    effect.EnableDefaultLighting();
                    effect.View = view;
                    effect.Projection = projection;
                    Matrix temp = transforms[mesh.ParentBone.Index];
                    effect.World = Matrix.CreateRotationZ(yardAngle) * temp * ShipTransform;
                }
                mesh.Draw();
            }
            foreach (ModelMesh mesh in rudders)
            {
                foreach (BasicEffect effect in mesh.Effects)
                {
                    effect.EnableDefaultLighting();
                    effect.View = view;
                    effect.Projection = projection;
                    Matrix temp = transforms[mesh.ParentBone.Index];
                    effect.World = Matrix.CreateRotationZ(-rudderAngle) * temp * ShipTransform;
                }
                mesh.Draw();
            }
            foreach (ModelMesh mesh in wheel)
            {
                foreach (BasicEffect effect in mesh.Effects)
                {
                    effect.EnableDefaultLighting();
                    effect.View = view;
                    effect.Projection = projection;
                    Matrix temp = transforms[mesh.ParentBone.Index];
                    effect.World = Matrix.CreateRotationY(rudderAngle * 10) * temp * ShipTransform;
                }
                mesh.Draw();
            }
        }
    }
}
